C++11 的 Memory Oder

最近也是一直准备秋招没怎么写博客,面试的情况也算一般吧,这周面完了百度和360,也不知道什么时候有结果,心累。

之前在看 《C++并发编程实战》的时候以及在C++并发学习/)的过程中就有学习过了,现在抽空把它总结一下。

在多核CPU,典型的系统架构如下:

它包含有2个CPU核,每个CPU核有一个私有的32KB 的 L1 cache,两个CPU 核共享 1MB的 L2 cache 以及 512MB的主存。

在这个内存模型下,cpu写数据并不是立即写入RAM中,而是写入L1 cache,再从L1 cache存入(store) RAM中,读数据也是先从L1 cache中读,读不到再从RAM中读,这种读写数据的模式是能够提高数据存取效率的,但是在一些特殊情况下会导致程序出错,考虑以下这个例子。

1
2
3
4
x = y = 0;
Thread 1 Thread 2
x = 1; y = 1;
r1 = y; r2 = x;

表面上看,r1 == r2 == 0 这种输出是不可能出现的,然而,有一种可能性是,由于r1不依赖于x,编译器可以把r1 = y这步操作调整到x = 1这步操作之前,同样,r2 = x这步操作可以调整到y = 1这步操作之前,这样一来,core 1可以先读取L1 cache中的y的值,core 2 才执行 y = 1的赋值操作,同理,r2 = x这步操作也可以在x = 1这步赋值操作之前执行,这时候就会出现r1 == r2 == 0的输出结果。

Memory Barrier

  • LoadLoad

  • StoreStore

  • LoadStore

  • StoreLoad

内存屏障用来代替互斥锁,既能保证程序的正确性,又能尽可能地提高程序执行效率

LoadLoad

LoadLoad 这种内存栅栏(memory barrier),顾名思义,就是阻止栅栏后面的load操作被调整到栅栏前面的load操作之前,类似于 git pull 或者 svn update 操作

LoadLoad 的主要作用是防止程序加载已经过期的数据,考虑以下代码:

1
2
3
4
if (IsPublished) // Load and check shared flag
{
LOADLOAD_FENCE(); // Prevent reordering of loads
return Value; // Load published value}

LOADLOAD_FENCE 在其中的作用是阻止读取Value这步操作被reorder到读取IsPublished这步操作之前,这样,只有在IsPublished置位后,才会去读取Value的值。

StoreStore

类似于LoadLoad,StoreStore 这种内存栅栏用于阻止栅栏后面的store操作被调整到栅栏前面的store操作之前,类似于git push或者svn commit操作

同理,StoreStore可以避免将过期的数据写入内存。

1
2
3
Value = x; // Publish some data
STORESTORE_FENCE();
IsPublished = 1; // Set shared flag to indicate availability of data

LoadStore

LoadStore 内存栅栏用于保证所有在这个栅栏之前的load操作一定会在这个栅栏之后的store操作之前执行。例如:

1
2
3
IsPublished = X; // Load X and set IsPublished
LOADSTORE_FENCE();
Value = 1; // Publish some data

在这里,Value = 1 这步操作可以被提前到读取X的值这步操作之前,之所以允许这种优化,是因为有时候在L1 cache中没有缓存X的值,而已经缓存了Value=1这步操作,这时候先执行store再执行load效率会更高。然而,LoadStore这种栅栏可以阻止这种情况的发生。

StoreLoad

StoreLoad 用于保证所有在这个栅栏之前的store操作一定会在这个栅栏之后的load操作之前执行,可以认为这是svn或者git中用户本地代码目录与central repository之间的一次同步操作

StoreLoad 可以解决前文所说的r1==r2==0的问题,考虑将程序改成如下这种形式。

1
2
3
4
5
x = y = 0;
Thread 1 Thread 2
x = 1; y = 1;
STORELOAD_FENCE(); STORELOAD_FENCE();
r1 = y; r2 = x;

在这种情况下,r1 == r2 == 0这个情况是不会出现的。

Acquire与Release语义

Acquire与Release是无锁编程中最容易混淆的两个原语,它们是线程之间合作进行数据操作的关键步骤。在这里,借助前面对memory barrier的解释,对acquire与release的语义进行阐述。

acquire

本质上是read-acquire,它只能应用在从RAM中read数据这种操作上,它确保了所有在acquire之后的语句不会被调整到它之前执行。

用上面的memory barrier来描述,acquire等价于LoadLoad加上LoadStore栅栏。

release

release本质上是write-release,它只能应用在write数据到RAM中,它确保了所有在release之前的语句不会被调整到它之后执行。

用上面的memory barrier来描述,release等价于LoadStore加上StoreStore栅栏。

互斥锁(mutex)

借助acquire与release语义,我们再重新来看一下互斥锁(mutex)如何用acquire与release来实现,实际上,mutex正是acquire与release这两个原语的由来,acquire的本意是acquire a lock,release的本意是release a lock,因此,互斥锁能保证被锁住的区域内得到的数据不会是过期的数据,而且所有写入操作在release之前一定会写入内存。

C++ 11中与memory order相关的同步操作

默认,排序一致:

  • memory_order_seq_cst

自由序列:

  • memory_order_relaxed,

获取-释放:

  • memory_order_acquire
  • memory_order_consume
  • memory_order_release
  • memory_order_acq_rel